首页 / 技术类 / COM / 裸写一个进程外 COM 组件

裸写一个进程外 COM 组件

2012-12-02 19:56:00

引言

前面九月份的八篇关于COM的文章,说的都是进程内COM。那时,我们从一个含内嵌IE控件的窗口说起,根据COM协议手工书写了进程内COM组件,并由此积累了一些类似ATL的框架性代码。

今天开始,我们把脚步迈向进程外组件。同样是从最基础的开始,本篇我们将根据进程外COM组件的加载规范手工编写一个EXE,然后用标准的COM调用方法来使用它。之前积累的框架性代码不属于第三方库,所以这里不会避免去使用,相反地,会把一些通用性较强的代码直接扩充到框架中。

本文仅限于常规EXE。NT服务程序暂时不在讨论之列。

命令行规范

进程外COM不像DLL,不需要实现四个导出函数。取而代之地,需要实现一些命令行参数:

  1. /RegServer
  2. /UnregServer
  3. /RegServerPerUser
  4. /UnRegServerPerUser

我没有找到官方文档,只是从ATL的实现来找到了上述四个参数。从名字来看,很容易理解。前两个相当于进程内组件的 DllRegisterServer 和 DllUnregisterServer,后两个针对当前用户,相当于 DllInstall(“user”)。

从ATL的实现代码来看,命令的前导符号不必是“/”,也可以是“-”。

另外,从测试情况来看,当COM库加载进程外组件的时候,会带上参数“-Embedding”,这可以用于区分用户主动运行还是被COM库加载。

注册和反注册

为了快速达到运行目的,今天我们只实现/RegServer和/UnregServer,后两个先不管了。

进程外COM和进程内COM的注册表结构大体一致,“粗看”发现,唯一的区别是,CLSID下的InprocServer32变成了LocalServer32。另外一个关键点是,我们自定义的每一个接口都需要注册到Interface下。Interface键结构:

第二个,TypeLib,跟CLSID的TypeLib一样。 第一个,ProxyStubClsid32,是要注册该接口的代理存根对象,用于序列化/反序列化参数和返回值。序列化/反序列化在COM中的术语是列集/散集,超不喜欢这名字。我们这里不实现自定义的代理存根,直接写死“{00020424-0000-0000-C000-000000000046}”,用系统的。不过使用这个代理存根有个局限,接口必须符合下列两种情况之一:

  1. 实现了IDispatch接口,并在IDL中把接口属性标记为dual。
  2. 只使用VARIANT兼容的数据类型,并在IDL中把接口属性标记为oleautomation。

另外说一点,ATL的 /RegServer,仅仅注册 dual 的接口。这点我们这里不学。

下面修改以前的ComModule::RegisterTypeLib,增加注册Interface的代码:

 1bool RegisterTypeLib(HKEY hRootKey)
 2{
 3    String strPath;
 4    strPath += _T("Software\\Classes\\TypeLib\\");
 5    strPath += m_strLibID;
 6    strPath += _T("\\");
 7    strPath += m_strLibVersion;
 8
 9    if (!Registry::SetString(hRootKey, strPath, _T(""), m_strLibName))
10    {
11        return false;
12    }
13
14    strPath += _T("\\0\\");
15#ifdef _WIN64
16    strPath += _T("Win64");
17#else
18    strPath += _T("Win32");
19#endif
20
21    if (!Registry::SetString(hRootKey, strPath, _T(""), m_strModulePath))
22    {
23        return false;
24    }
25
26    for (UINT i = 0; i < m_pTypeLib->GetTypeInfoCount(); ++i)
27    {
28        TYPEKIND type = TKIND_MAX;
29        HRESULT hr = m_pTypeLib->GetTypeInfoType(i, &type);
30
31        if (FAILED(hr))
32        {
33            return false;
34        }
35
36        if (type != TKIND_INTERFACE && type != TKIND_DISPATCH)
37        {
38            continue;
39        }
40
41        ITypeInfo *pTypeInfo = nullptr;
42        hr = m_pTypeLib->GetTypeInfo(i, &pTypeInfo);
43
44        if (FAILED(hr))
45        {
46            return false;
47        }
48
49        XL_ON_BLOCK_EXIT(pTypeInfo, &ITypeInfo::Release);
50
51        TYPEATTR *pAttr = nullptr;
52        pTypeInfo->GetTypeAttr(&pAttr);
53
54        if (FAILED(hr))
55        {
56            return false;
57        }
58
59        XL_ON_BLOCK_EXIT(pTypeInfo, &ITypeInfo::ReleaseTypeAttr, pAttr);
60
61        TCHAR szInterfaceID[40] = {};
62        StringFromGUID2(pAttr->guid, szInterfaceID, ARRAYSIZE(szInterfaceID));
63
64        String strInterfacePath;
65        strInterfacePath += _T("Software\\Classes\\Interface\\");
66        strInterfacePath += szInterfaceID;
67
68        if (!Registry::SetString(hRootKey, strInterfacePath + _T("\\ProxyStubClsid32"), _T(""), _T("{00020424-0000-0000-C000-000000000046}")))
69        {
70            return false;
71        }
72
73        if (!Registry::SetString(hRootKey, strInterfacePath + _T("\\TypeLib"), _T(""), m_strLibID.GetAddress()))
74        {
75            return false;
76        }
77
78        if (!Registry::SetString(hRootKey, strInterfacePath + _T("\\TypeLib"), _T("Version"), m_strLibVersion.GetAddress()))
79        {
80            return false;
81        }
82    }
83
84    return true;
85}

反注册相应地增加删除Interface的代码:

 1bool UnregisterTypeLib(HKEY hRootKey)
 2{
 3    for (UINT i = 0; i < m_pTypeLib->GetTypeInfoCount(); ++i)
 4    {
 5        TYPEKIND type = TKIND_MAX;
 6        HRESULT hr = m_pTypeLib->GetTypeInfoType(i, &type);
 7
 8        if (FAILED(hr))
 9        {
10            return false;
11        }
12
13        if (type != TKIND_INTERFACE && type != TKIND_DISPATCH)
14        {
15            continue;
16        }
17
18        ITypeInfo *pTypeInfo = nullptr;
19        hr = m_pTypeLib->GetTypeInfo(i, &pTypeInfo);
20
21        if (FAILED(hr))
22        {
23            return false;
24        }
25
26        XL_ON_BLOCK_EXIT(pTypeInfo, &ITypeInfo::Release);
27
28        TYPEATTR *pAttr = nullptr;
29        pTypeInfo->GetTypeAttr(&pAttr);
30
31        if (FAILED(hr))
32        {
33            return false;
34        }
35
36        XL_ON_BLOCK_EXIT(pTypeInfo, &ITypeInfo::ReleaseTypeAttr, pAttr);
37
38        TCHAR szInterfaceID[40] = {};
39        StringFromGUID2(pAttr->guid, szInterfaceID, ARRAYSIZE(szInterfaceID));
40
41        String strInterfacePath;
42        strInterfacePath += _T("Software\\Classes\\Interface\\");
43        strInterfacePath += szInterfaceID;
44
45        if (!Registry::DeleteKeyRecursion(hRootKey, strInterfacePath))
46        {
47            return false;
48        }
49    }
50
51    String strPath;
52    strPath += _T("Software\\Classes\\TypeLib\\");
53    strPath += m_strLibID;
54
55    if (!Registry::DeleteKeyRecursion(hRootKey, strPath))
56    {
57        return false;
58    }
59
60    return true;      
61}

再给RegisterComClasses新增个参数:

1bool RegisterComClasses(HKEY hRootKey, bool bInprocServer = true)

相应注册逻辑修改如下:

 1if (bInprocServer)
 2{
 3    if (!Registry::SetString(hRootKey, strClassIDPath + _T("\\InprocServer32"), _T(""), m_strModulePath))
 4    {
 5        return false;
 6    }
 7}
 8else
 9{
10    if (!Registry::SetString(hRootKey, strClassIDPath + _T("\\LocalServer32"), _T(""), m_strModulePath))
11    {
12        return false;
13    }
14}

然后,汇总一下,写两个供调用的接口函数:

 1STDMETHODIMP ExeRegisterServer()
 2{
 3    if (!RegisterTypeLib(HKEY_LOCAL_MACHINE))
 4    {
 5        return E_FAIL;
 6    }
 7
 8    if (!RegisterComClasses(HKEY_LOCAL_MACHINE, false))
 9    {
10        return E_FAIL;
11    }
12
13    return S_OK;
14}
15      
16STDMETHODIMP ExeUnregisterServer()
17{
18    if (!UnregisterComClasses(HKEY_LOCAL_MACHINE))
19    {
20        return E_FAIL;
21    }
22
23    if (!UnregisterTypeLib(HKEY_LOCAL_MACHINE))
24    {
25        return E_FAIL;
26    }
27
28    return S_OK;
29}

对于PerUser的情形,只需要把四处HKEY_LOCAL_MACHINE换成HKEY_CURRENT_USER就好了,不过现在我们先不管这些。

然后我们转到入口函数WinMain,加上对/RegServer和/UnregServer的处理:

 1int APIENTRY _tWinMain(_In_ HINSTANCE     hInstance,
 2                       _In_opt_ HINSTANCE hPrevInstance,
 3                       _In_ LPTSTR        lpCmdLine,
 4                       _In_ int           nCmdShow)
 5{
 6    xl::g_pComModule = new xl::ComModule(hInstance, _T("Streamlet COMProvider TypeLib 1.0"));
 7  
 8    if (_tcsicmp(lpCmdLine, _T("/RegServer")) == 0 || _tcsicmp(lpCmdLine, _T("-RegServer")) == 0)
 9    {
10        xl::g_pComModule->ExeRegisterServer();
11    }
12    else if (_tcsicmp(lpCmdLine, _T("/UnregServer")) == 0 || _tcsicmp(lpCmdLine, _T("-UnregServer")) == 0)
13    {
14        xl::g_pComModule->ExeUnregisterServer();
15    }
16
17    delete xl::g_pComModule;
18
19    return 0;
20}

到目前为止,可以编译程序,把组件注册上了。

进程外COM的启动

进程外COM的启动大致有如下几步:

  1. 初始化COM库。
  2. 使用CoRegisterClassObject向COM库注册类厂。
  3. 跑一个消息循环,并自己控制退出。
  4. 调用CoRevokeClassObject向COM库注消类厂。(Revoke这个名字也不喜欢,一般与Register对应的都是Unregister。)
  5. 反初始化COM库。
  6. 退出。

我们将WinMain改成如下的样子:

 1int APIENTRY _tWinMain(_In_ HINSTANCE     hInstance,
 2                       _In_opt_ HINSTANCE hPrevInstance,
 3                       _In_ LPTSTR        lpCmdLine,
 4                       _In_ int           nCmdShow)
 5{
 6    xl::g_pComModule = new xl::ComModule(hInstance, _T("Streamlet COMProvider TypeLib 1.0"));
 7  
 8    if (_tcsicmp(lpCmdLine, _T("/RegServer")) == 0 || _tcsicmp(lpCmdLine, _T("-RegServer")) == 0)
 9    {
10        xl::g_pComModule->ExeRegisterServer();
11    }
12    else if (_tcsicmp(lpCmdLine, _T("/UnregServer")) == 0 || _tcsicmp(lpCmdLine, _T("-UnregServer")) == 0)
13    {
14        xl::g_pComModule->ExeUnregisterServer();
15    }
16    else if (_tcsicmp(lpCmdLine, _T("/Embedding")) == 0 || _tcsicmp(lpCmdLine, _T("-Embedding")) == 0)
17    {
18        HRESULT hr = xl::g_pComModule->ExeRegisterClassObject();
19
20        if (SUCCEEDED(hr))
21        {
22            MSG msg = {};
23
24            while (GetMessage(&msg, nullptr, 0, 0))
25            {
26                TranslateMessage(&msg);
27                DispatchMessage(&msg);
28            }
29        }
30
31        xl::g_pComModule->ExeUnregisterClassObject();
32    }
33
34    delete xl::g_pComModule;
35
36    return 0;
37}

我们将在ComModule::ExeRegisterClassObject里完成1、2,在ComModule::ExeUnregisterClassObject里完成4、5。

ComModule::ExeRegisterClassObject

之前为了注册COM类,我们已经保存了对象表,里面有每一个对外暴露的类的CLSID、类厂创建函数指针等。我们找到每一个要注册的类,创建类厂,然后调用CoRegisterClassObject。CoRegisterClassObject将返回一个DWORD值(Cookie),用于唯一确定所注册的类,在CoRevokeClassObject的时候要用到,所以要保存起来。CoRegisterClassObject内部会将我们传给它的类厂的引用计数加一,我们应该把自己产生的引用计数都释放掉,整个COM组件运行期间,类厂引用计数始终维持在1,就是COM库占用的那个。

代码如下:

 1STDMETHODIMP ExeRegisterClassObject()
 2{
 3    HRESULT hr = CoInitialize(nullptr);
 4
 5    if (FAILED(hr))
 6    {
 7        return hr;
 8    }
 9
10    for (const ClassEntry * const *ppEntry = &LP_CLASS_BEGIN + 1; ppEntry < &LP_CLASS_END; ++ppEntry)
11    {
12        if (*ppEntry == nullptr)
13        {
14            continue;
15        }
16
17        IClassFactory *pClassFactory = (*ppEntry)->pfnCreator();
18
19        if (pClassFactory == nullptr)
20        {
21            return E_FAIL;
22        }
23
24        IUnknown *pUnk = nullptr;
25        HRESULT hr = pClassFactory->QueryInterface(__uuidof(IUnknown), (LPVOID *)&pUnk);
26
27        if (FAILED(hr) || pUnk == nullptr)
28        {
29            return hr;
30        }
31
32        DWORD dwRegister = 0;
33        hr = CoRegisterClassObject(*(*ppEntry)->pClsid,
34                                    pUnk,
35                                    CLSCTX_LOCAL_SERVER,
36                                    REGCLS_MULTIPLEUSE,
37                                    &dwRegister);
38        pUnk->Release();
39
40        if (FAILED(hr))
41        {
42            return hr;
43        }
44
45        m_arrRegClassObjects.PushBack(dwRegister);
46    }
47
48    return S_OK;
49}

ComModule::ExeUnregisterClassObject

这个就比较简单了,针对上面保存的Cookie,调用CoRevokeClassObject即可。CoRevokeClassObject会调用类厂的Release来释放COM库占用的那个引用计数。

 1STDMETHODIMP ExeUnregisterClassObject()
 2{
 3    for (auto it = m_arrRegClassObjects.Begin(); it != m_arrRegClassObjects.End(); ++it)
 4    {
 5        CoRevokeClassObject(*it);
 6    }
 7
 8    m_arrRegClassObjects.Clear();
 9
10    CoUninitialize();
11
12    return S_OK;
13}

进程外COM的使用

通过上面几个步骤,我们的进程外COM组件已经可以被使用了。为了检验参数的序列化/反序列化是否正确,我们稍稍地改变下接口ISampleInterface,加些参数:

[
    object,
    uuid(83C783E3-F989-4E0D-BFC5-631273EDFFDB),
    dual,
]
interface ISampleInterface : IDispatch
{
    [id(1)] HRESULT SampleMethod([in] BSTR bstrMessage, [out] LONG *pResult);
};

并实现如下:

 1STDMETHODIMP SampleClass::SampleMethod(BSTR bstrMessage, LONG *pResult)
 2{
 3    MessageBox(NULL, bstrMessage, _T("Info"), MB_OK | MB_ICONINFORMATION);
 4
 5    if (pResult != nullptr)
 6    {
 7        *pResult = 12345678;
 8    }
 9
10    return S_OK;
11}

另起一个EXE:

 1int _tmain(int argc, TCHAR *argv[])
 2{
 3    HRESULT hr = CoInitialize(NULL);
 4
 5    if (FAILED(hr))
 6    {
 7        return 0;
 8    }
 9  
10    ISampleInterface *pSampleInterface = nullptr;
11    hr = CoCreateInstance(__uuidof(SampleClass),
12                          nullptr,
13                          CLSCTX_LOCAL_SERVER,
14                          __uuidof(ISampleInterface),
15                          (LPVOID *)&pSampleInterface);
16
17    if (SUCCEEDED(hr))
18    {
19        BSTR bstrMessage = SysAllocString(_T("COMProvider!SampleClass::SampleMethod Called From COMUser."));
20        LONG nResult = 0;
21        pSampleInterface->SampleMethod(bstrMessage, &nResult);
22        SysFreeString(bstrMessage);
23        pSampleInterface->Release();
24    }
25
26    CoUninitialize();
27
28    return 0;
29}

运行结果:

还有一个输出参数值:

一切正常。

但是此时,调用方运行结束后,COM组件还在运行中,没有退出。退出是需要我们手工控制的,下面我们来做这件事。

进程外COM的退出

观察了下ATL的实现,它是在最后一个对象被释放后,触发AtlExeModuleT的Unlock,在其中向主线程发送了一个WM_QUIT,结束消息循环。

我们之前实现进程内组件的时候,也做过DllCanUnloadNow,这里面,有一个对象计数和类厂的锁计数。而由于xl::ComClass在构造的时候就对对象技术进行了加一,所以对象计数包含了类厂的计数,这在进程内组件里没问题,而且也是必须的。因为类厂存在,说明被使用,DLL不该被释放。

而在进程外组件中,由于消息循环结束之前,COM库肯定会占用一个类厂引用计数,如果对象计数包含类厂的话,我们就无法判断发WM_QUIT的时机了。因此,我们这里对xl::ComClass的构造函数和加一个参数,指定需不需要加引用计数,然后分别在DLL创建类厂和EXE创建类厂的时候传入不同的值。

xl::ComClass 构造析构函数修改如下:

 1ComClass(bool bAddObjRefCount = true) : m_nRefCount(0), m_bAddObjRefCount(bAddObjRefCount)
 2{
 3    if (g_pComModule != nullptr && m_bAddObjRefCount)
 4    {
 5        g_pComModule->ObjectAddRef();
 6    }
 7}
 8
 9~ComClass()
10{
11    if (g_pComModule != nullptr && m_bAddObjRefCount)
12    {
13        g_pComModule->ObjectRelease();
14    }
15}

类厂构造函数和创建函数修改如下:

 1static IClassFactory *CreateFactory(bool bAddObjRefCount = true)
 2{
 3    return new ClassFactory(bAddObjRefCount);
 4}
 5
 6ClassFactory(bool bAddObjRefCount = true) :
 7    ComClass<ClassFactory<T>>(bAddObjRefCount)
 8{
 9
10}
11
12xl::ComModule::ExeRegisterClassObject中修改如下:
13
14STDMETHODIMP ExeRegisterClassObject()
15{
16    HRESULT hr = CoInitialize(nullptr);
17
18    if (FAILED(hr))
19    {
20        return hr;
21    }
22
23    for (const ClassEntry * const *ppEntry = &LP_CLASS_BEGIN + 1; ppEntry < &LP_CLASS_END; ++ppEntry)
24    {
25        if (*ppEntry == nullptr)
26        {
27            continue;
28        }
29
30        IClassFactory *pClassFactory = (*ppEntry)->pfnCreator(false);
31
32        if (pClassFactory == nullptr)
33        {
34            return E_FAIL;
35        }
36
37        IUnknown *pUnk = nullptr;
38        HRESULT hr = pClassFactory->QueryInterface(__uuidof(IUnknown), (LPVOID *)&pUnk);
39
40        if (FAILED(hr) || pUnk == nullptr)
41        {
42            return hr;
43        }
44
45        DWORD dwRegister = 0;
46        hr = CoRegisterClassObject(*(*ppEntry)->pClsid,
47                                    pUnk,
48                                    CLSCTX_LOCAL_SERVER,
49                                    REGCLS_MULTIPLEUSE,
50                                    &dwRegister);
51        pUnk->Release();
52
53        if (FAILED(hr))
54        {
55            return hr;
56        }
57
58        m_arrRegClassObjects.PushBack(dwRegister);
59    }
60
61    m_dwThreadId = GetCurrentThreadId();
62
63    return S_OK;
64}

注意在最后存了一个Thread ID,这个ID表明当前是处于EXE模式。

然后在xl::ComModule的对象引用计数释放、锁计数释放的函数中判断并发送WM_QUIT:

 1ULONG STDMETHODCALLTYPE ObjectRelease()
 2{
 3    ULONG lResult = (ULONG)InterlockedDecrement(&m_nObjectRefCount);
 4
 5    if (m_dwThreadId != 0 && CanUnloadNow())
 6    {
 7        PostThreadMessage(m_dwThreadId, WM_QUIT, 0, 0);
 8    }
 9
10    return lResult;
11}
12
13ULONG STDMETHODCALLTYPE LockRelease()
14{
15    ULONG lResult = (ULONG)InterlockedDecrement(&m_nLockRefCount);
16
17    if (m_dwThreadId != 0 && CanUnloadNow())
18    {
19        PostThreadMessage(m_dwThreadId, WM_QUIT, 0, 0);
20    }
21
22    return lResult;
23}

其中CanUnloadNow跟DllCanUnloadNow的判断是一致的,于是乎合并起来:

 1bool CanUnloadNow()
 2{
 3    if (m_nObjectRefCount > 0 || m_nLockRefCount > 0)
 4    {
 5        return false;
 6    }
 7
 8    return true;
 9}
10
11STDMETHODIMP DllCanUnloadNow()
12{
13    return CanUnloadNow() ? S_OK : S_FALSE;
14}

好了,代码实现全部结束。

从不同语言调用

WSH+VBScript

和以前差不多,也写个VBS脚本:

Set obj = WScript.CreateObject("Streamlet.COMProvider.SampleClass.1")
obj.SampleMethod "Hello! Calling from VBScript.", 0

运行结果:

Visual Basic 6

VB6代码:

Private Sub Command1_Click()

    Dim obj As Object
    Set obj = CreateObject("Streamlet.COMProvider.SampleClass.1")
    obj.SampleMethod "Hello! Calling from VB6.", 0
    Set obj = Nothing

End Sub

运行结果:

网页中的Javascript

JS代码如下:

1<script type='text/javascript'>
2    var objCom = new ActiveXObject("Streamlet.COMProvider.SampleClass.1");
3    objCom.SampleMethod("Hello! Calling from JavaScript.", 0);
4</script>

运行结果:

遗憾的是,从网页调用后,COM组件似乎没法退出。查了下,貌似是JS释放对象机制的问题。

本文例子代码见:COMProtocol5.rarhttp://pan.baidu.com/s/1dD3ZzUD


首发:http://www.cppblog.com/Streamlet/archive/2012/12/02/195900.html



NoteIsSite/0.4